package com.zhihao.config;

import com.zhihao.types.annotations.DCCValue;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.apache.curator.framework.recipes.cache.CuratorCacheListener;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * 基于 Zookeeper 的配置中心实现原理
 */
@Slf4j
//@Configuration    // 交由ZookeeperClientConfig控制DCCValueBeanFactory配置类 是否进行初始化及创建Bean
public class DCCValueBeanFactory implements BeanPostProcessor {

    /*
        BeanPostProcessor接口：
            通过实现该接口，可以在 Spring Bean 初始化前后插入自定义逻辑。这里只重写了 postProcessAfterInitialization 方法，用于处理所有 Bean 初始化后的逻辑。
            由于Controller中有用到自定义注解即切面类，所以会把该controller对象被Spring proxy代理类代理，所以需要注意
     */

    /* Zookeeper 的根路径 /big-market-dcc */
    private static final String BASE_CONFIG_PATH = "/big-market-dcc";
    /* 配置节点的完整路径 /big-market-dcc/config */
    private static final String BASE_CONFIG_PATH_CONFIG = BASE_CONFIG_PATH + "/config";

    /* 缓存被 @DCCValue 注解标记的 Bean 实例，键key为Zookeeper节点路径，值Value为被监听配置的自定义注解标记的类 */
    /* eg:Map<[ /big-market-dcc/config/degradeSwitch, RaffleActivityController]...> */
    private final Map<String, Object> dccObjGroup = new HashMap<>();

    /* zookeeper的Java API Client */
    // 如果使用这种不强制检查bean是否为空的话，则需要自己在代码里面手动处理
//    @Autowired(required = false)    // 由于zk启动时负载压力大，设置zk不随系统启动后，需要使用这个注解标记 不强制要求Bean注入成功
    private CuratorFramework client;

    /**
     * 构造函数，类加载时的构造器
     *
     * 1.初始化 Zookeeper 节点
     *  检查 /big-market-dcc/config 是否存在，不存在则创建。
     * 2.启动节点监听
     *  使用 CuratorCache 监听 /big-market-dcc/config 下的所有子节点变化。
     * 3.监听节点变化事件
     *  当节点数据变化时（NODE_CHANGED），从 dccObjGroup 缓存中找到对应的 Bean，通过反射更新其字段值。
     *
     * @author Jackson
     * @date 2025-05-25 19:57
     **/
    public DCCValueBeanFactory(CuratorFramework client) throws Exception {
        if (client == null) return;
        this.client = client;

        // 1. ZNode节点初始化
        if (null == client.checkExists().forPath(BASE_CONFIG_PATH_CONFIG)) {
            client.create().creatingParentsIfNeeded().forPath(BASE_CONFIG_PATH_CONFIG);
            log.info("DCC 节点监听 base node {} not absent create new done!", BASE_CONFIG_PATH_CONFIG);
        }
        // 2. 创建节点监听器-创建NodeCache
        CuratorCache curatorCache = CuratorCache.build(client, BASE_CONFIG_PATH_CONFIG);
        // 开启监听（可以在开启监听前/后添加监听器listener）
        curatorCache.start();
        // 3. 监听节点变化事件-注册监听
        curatorCache.listenable().addListener(new CuratorCacheListener() {
            @Override
            public void event(Type type, ChildData oldData, ChildData data) {
                switch (type) {
                    case NODE_CHANGED:  //节点数据变更监听
                        // 获取变更节点ZNode的Path全路径
                        String dccValueZNodePath = data.getPath();
                        // 旧值
                        String oldDCCValue = new String(oldData.getData());
                        // 新值
                        String newDCCValue = new String(data.getData());
                        // 从自定义存储Map中获取ZNode Path对应的配置类对象
                        Object objBean = dccObjGroup.get(dccValueZNodePath);
                        if (null == objBean) return;
                        try {
                            // 处理代理对象的类 eg:RaffleActivityController
                            Class<?> objBeanClass = objBean.getClass();
                            // 检查 objBean 是否是代理对象
                            if (AopUtils.isAopProxy(objBean)) {
                                // 获取代理对象的目标对象
                                objBeanClass = AopUtils.getTargetClass(objBean);
//                            objBeanClass = AopProxyUtils.ultimateTargetClass(objBean);
                            }
                            /* 反射更新字段值 */
                            // 1. getDeclaredField 方法用于获取指定类中声明的“所有”字段，包括私有字段、受保护字段和公共字段。
                            // 2. getField 方法用于获取指定类中的”公共“字段，即只能获取到公共访问修饰符（public）的字段。
                            // 从节点路径中提取指定key的字段名（如 /big-market-dcc/config/key 提取 key），通过反射设置字段值。 eg: degradeSwitch
                            String fieldName = dccValueZNodePath.substring(dccValueZNodePath.lastIndexOf("/") + 1);
                            Field field = objBeanClass.getDeclaredField(fieldName);
                            field.setAccessible(true);
                            field.set(objBean, new String(data.getData()));
                            field.setAccessible(false);
                            log.info(objBeanClass.getName() + "中的" + fieldName + "配置更新成功:" + oldDCCValue +" -> " + newDCCValue);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                        break;
                    default:
                        break;
                }
            }
        });
    }

    /*
        通过重写Bean初始化的后处理方法，可以自定义实现一些功能
        eg：DCC中在Bean加载后可以检测某个Bean中的属性是否被@DCCValue注解修饰，从而初始化创建ZK的ZNode节点及存储类对象至Map等

    作用：
        1.处理 AOP 代理对象
            通过 AopUtils 获取目标类和目标对象，确保能正确访问原始类的字段。
        2.解析 @DCCValue 注解
            提取注解中的配置键（key）和默认值（defaultValue）。
        3.初始化 Zookeeper 节点
            若节点不存在则创建，并根据默认值初始化字段。
        4.动态注入配置值
            从 Zookeeper 节点读取最新值，通过反射设置到字段中。
        5.缓存 Bean 实例
            将配置路径和 Bean 实例存入 dccObjGroup，供监听器更新时使用。
     * @author Jackson
     * @date 2025-05-25 19:19
     **/
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (client == null) return bean;

        /*
            引入AOP切面后，被加“注解”的类会被“切面”管理，则该类会成为一个“代理类”。
            代理类是无法直接获取类中属性值的！！！
            即：  Class<?> beanClass = bean.getClass(); beanClass此时返回的是“代理类”
                后续获取DCCValue值的时候是无法获取到的，所以需要额外处理。
         */
        // 注意；增加 AOP 代理后，获得类的方式要通过 AopProxyUtils.getTargetClass(bean); 不能直接 bean.class 因为代理后类的结构发生变化，这样不能获得到自己的自定义注解了。
        Class<?> targetBeanClass = bean.getClass();
        Object targetBeanObject = bean;
        if (AopUtils.isAopProxy(bean)) {
            targetBeanClass = AopUtils.getTargetClass(bean);
            targetBeanObject = AopProxyUtils.getSingletonTarget(bean);
        }
//        Class<?> beanClass = bean.getClass();
        Field[] fields = targetBeanClass.getDeclaredFields();
        // 循环检测Bean类中属性有无被@DCCValue注解修饰
        for (Field field : fields) {
            // 1. 未被修饰，跳过
            if (!field.isAnnotationPresent(DCCValue.class)) {
                continue;
            }
            // 2. 检测到@DCCValue注解的属性
            DCCValue dccValue = field.getAnnotation(DCCValue.class);
            // 获取注解中的value()值
            String value = dccValue.value();
            if (StringUtils.isBlank(value)) {
                throw new RuntimeException(field.getName() + " @DCCValue is not config value config case 「isSwitch/isSwitch:1」");
            }
            String[] splits = value.split(":");
            // 降级key degradeSwitch
            String key = splits[0];
            // 项目中的初始化默认值value close
            String defaultValue = splits.length == 2 ? splits[1] : null;
            // 连接ZK配置中心，随系统启动创建zk中相应的存储节点ZNode
            try {
                String keyPath = BASE_CONFIG_PATH_CONFIG.concat("/").concat(key);
                // 判断当前节点是否存在，不存在则创建出 Zookeeper 节点， 存在则使用zk中的配置值
                if (null == client.checkExists().forPath(keyPath)) {
                    client.create().creatingParentsIfNeeded().forPath(keyPath);
                    // todo:自行补充zk对应key节点的初始化值，znode初始化值为系统配置项的默认值;
                    //  防止系统启动仅创建znode节点未在zk中设置初始值，会zk节点会默认显示znode创建节点的操作机ip
                    client.setData().forPath(keyPath,defaultValue.getBytes(StandardCharsets.UTF_8));
                    if (StringUtils.isNotBlank(defaultValue)) {
                        field.setAccessible(true);
                        field.set(targetBeanObject, defaultValue);
                        field.setAccessible(false);
                    }
                    // eg:  DCC 节点监听 创建节点 /big-market-dcc/config/rateLimiterSwitch
                    // eg:  DCC 节点监听 创建节点 /big-market-dcc/config/degradeSwitch
                    log.info("DCC 节点监听 创建节点 {}", keyPath);
                } else {
                    // 如果zk的ZNode节点已存在，则有限使用zk中的
                    String configValue = new String(client.getData().forPath(keyPath));
                    if (StringUtils.isNotBlank(configValue)) {
                        field.setAccessible(true);
                        field.set(targetBeanObject, configValue);
                        field.setAccessible(false);
                        log.info("DCC 节点监听 设置配置 {} {} {}", keyPath, field.getName(), configValue);
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            // 将@DCCValue配置的需要动态配置的属性key及所在的类对象 存储到Map中供后期使用
            dccObjGroup.put(BASE_CONFIG_PATH_CONFIG.concat("/").concat(key), targetBeanObject);
        }
        return bean;
    }

}
